﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using Mono.Reflection;

namespace DelegateExpressionizer
{
    public class InstructionParser
    {
        private const string CachedAnonymousMethodDelegate = "<>9__CachedAnonymousMethodDelegate";

        public static InstructionParser Create(ParameterExpression[] locals, IList<ParameterExpression> args)
        {
            return new InstructionParser(new Stack<Expression>(), locals, args);
        }

        InstructionParser Clone()
        {
            return new InstructionParser(new Stack<Expression>(/* stack */), locals, args.ToArray());
        }

        static readonly MethodInfo StringConcat = typeof(string).GetMethod("Concat", new[] { typeof(object), typeof(object) });

        readonly Stack<Expression> stack;
        readonly ParameterExpression[] locals;
        readonly IList<ParameterExpression> args;
        private Instruction instruction;
        private Dictionary<OpCode, Action> table;

        InstructionParser(Stack<Expression> stack, ParameterExpression[] locals, IList<ParameterExpression> args)
        {
            this.stack = stack;
            this.locals = locals;
            this.args = args;
            CreateHashMap();
        }

        void Push(Expression e)
        {
            stack.Push(e);
        }

        Expression Pop()
        {
            return stack.Pop();
        }

        public Expression Process(Instruction start, Type returnType)
        {
            var ex = AdjustType(Process(start), returnType);

            if (ex.Type != returnType && returnType != typeof(void))
            {
                return Expression.Convert(ex, returnType);
            }

            return ex;
        }

        void CreateHashMap()
        {
            table = new Dictionary<OpCode, Action>
	        {
	            {OpCodes.Nop, Nop},
	            {OpCodes.Break, Break},
	            {OpCodes.Ldarg_0, () => LdArg(0)},
	            {OpCodes.Ldarg_1, () => LdArg(1)},
	            {OpCodes.Ldarg_2, () => LdArg(2)},
	            {OpCodes.Ldarg_3, () => LdArg(3)},
	            {OpCodes.Ldarg_S, () => LdArg((short) instruction.Operand)},
	            {OpCodes.Ldarg, () => LdArg((int) instruction.Operand)},
	            {OpCodes.Ldarga, Ldarga},
	            {OpCodes.Ldarga_S, Ldarga},
	            {OpCodes.Ldlen, Ldlen},
	            {OpCodes.Ldelem, LdElem},
	            {OpCodes.Ldelem_I, LdElem},
	            {OpCodes.Ldelem_I1, LdElem},
	            {OpCodes.Ldelem_I2, LdElem},
	            {OpCodes.Ldelem_I4, LdElem},
	            {OpCodes.Ldelem_I8, LdElem},
	            {OpCodes.Ldelem_U1, LdElem},
	            {OpCodes.Ldelem_U2, LdElem},
	            {OpCodes.Ldelem_U4, LdElem},
	            {OpCodes.Ldelem_R4, LdElem},
	            {OpCodes.Ldelem_R8, LdElem},
	            {OpCodes.Ldelem_Ref, LdElem},
	            {OpCodes.Stloc_0, () => StLoc(0)},
	            {OpCodes.Stloc_1, () => StLoc(1)},
	            {OpCodes.Stloc_2, () => StLoc(2)},
	            {OpCodes.Stloc_3, () => StLoc(3)},
	            {OpCodes.Stloc_S, () => StLoc((byte) instruction.Operand)},
	            {OpCodes.Stloc, () => StLoc((int) instruction.Operand)},
	            {OpCodes.Stelem, StElem},
	            {OpCodes.Stelem_I, StElem},
	            {OpCodes.Stelem_I1, StElem},
	            {OpCodes.Stelem_I2, StElem},
	            {OpCodes.Stelem_I4, StElem},
	            {OpCodes.Stelem_I8, StElem},
	            {OpCodes.Stelem_R4, StElem},
	            {OpCodes.Stelem_R8, StElem},
	            {OpCodes.Stelem_Ref, StElem},
	            {OpCodes.Ldnull, LdNull},
	            {OpCodes.Ldfld, LdFld},
	            {OpCodes.Ldflda, LdFld},
	            {OpCodes.Ldsfld, Ldsfld},
	            {OpCodes.Ldloc_0, () => LdLoc(0)},
	            {OpCodes.Ldloc_1, () => LdLoc(1)},
	            {OpCodes.Ldloc_2, () => LdLoc(2)},
	            {OpCodes.Ldloc_3, () => LdLoc(3)},
	            {OpCodes.Ldloc, () => LdLoc((int) instruction.Operand)},
	            {OpCodes.Ldloc_S, () => LdLoc(((LocalVariableInfo) instruction.Operand).LocalIndex)},
	            {OpCodes.Ldloca, LdLoca},
	            {OpCodes.Ldloca_S, LdLoca},
	            {OpCodes.Ldstr, Ldstr},
	            {OpCodes.Ldc_I4_0, () => LdC(0)},
	            {OpCodes.Ldc_I4_1, () => LdC(1)},
	            {OpCodes.Ldc_I4_2, () => LdC(2)},
	            {OpCodes.Ldc_I4_3, () => LdC(3)},
	            {OpCodes.Ldc_I4_4, () => LdC(4)},
	            {OpCodes.Ldc_I4_5, () => LdC(5)},
	            {OpCodes.Ldc_I4_6, () => LdC(6)},
	            {OpCodes.Ldc_I4_7, () => LdC(7)},
	            {OpCodes.Ldc_I4_8, () => LdC(8)},
	            {OpCodes.Ldc_I4_S, () => LdC((sbyte) instruction.Operand)},
	            {OpCodes.Ldc_I4_M1, () => LdC(-1)},
	            {OpCodes.Ldc_I4, () => LdC((int) instruction.Operand)},
	            {OpCodes.Ldc_I8, () => LdC((long) instruction.Operand)},
	            {OpCodes.Ldc_R4, () => LdC((float) instruction.Operand)},
	            {OpCodes.Ldc_R8, () => LdC((double) instruction.Operand)},
	            {OpCodes.Br_S, Br},
	            {OpCodes.Br, Br},
	            {OpCodes.Brfalse, Brfalse},
	            {OpCodes.Brfalse_S, Brfalse},
	            {OpCodes.Brtrue, Brtrue},
	            {OpCodes.Brtrue_S, Brtrue},
	            {OpCodes.Bgt, Bgt},
	            {OpCodes.Bgt_S, Bgt},
	            {OpCodes.Bgt_Un, Bgt},
	            {OpCodes.Bgt_Un_S, Bgt},
	            {OpCodes.Bge, Bge},
	            {OpCodes.Bge_S, Bge},
	            {OpCodes.Bge_Un, Bge},
	            {OpCodes.Bge_Un_S, Bge},
	            {OpCodes.Blt, Blt},
	            {OpCodes.Blt_S, Blt},
	            {OpCodes.Blt_Un, Blt},
	            {OpCodes.Blt_Un_S, Blt},
	            {OpCodes.Ble, Ble},
	            {OpCodes.Ble_S, Ble},
	            {OpCodes.Ble_Un, Ble},
	            {OpCodes.Ble_Un_S, Ble},
	            {OpCodes.Beq, Beq},
	            {OpCodes.Beq_S, Beq},
	            {OpCodes.Bne_Un, BneUn},
	            {OpCodes.Bne_Un_S, BneUn},
	            {OpCodes.Dup, Dup},
	            {OpCodes.Pop, Pop_},
	            {OpCodes.Add, Add},
	            {OpCodes.Add_Ovf, Add_Ovf},
	            {OpCodes.Add_Ovf_Un, Add_Ovf},
	            {OpCodes.Sub, Sub},
	            {OpCodes.Sub_Ovf, Sub_Ovf},
	            {OpCodes.Sub_Ovf_Un, Sub_Ovf},
	            {OpCodes.Mul, Mul},
	            {OpCodes.Mul_Ovf, Mul_Ovf},
	            {OpCodes.Mul_Ovf_Un, Mul_Ovf},
	            {OpCodes.Div, Div},
	            {OpCodes.Div_Un, Div},
	            {OpCodes.Rem, Rem},
	            {OpCodes.Rem_Un, Rem},
	            {OpCodes.Xor, Xor},
	            {OpCodes.Shl, Shl},
	            {OpCodes.Shr, Shr},
	            {OpCodes.Shr_Un, Shr},
	            {OpCodes.Neg, Neg},
	            {OpCodes.Not, Not},
	            {OpCodes.And, And},
	            {OpCodes.Or, Or},
	            {OpCodes.Conv_I, () => Conv(typeof (int))},
	            {OpCodes.Conv_I1, () => Conv(typeof (sbyte))},
	            {OpCodes.Conv_I2, () => Conv(typeof (short))},
	            {OpCodes.Conv_I4, () => Conv(typeof (int))},
	            {OpCodes.Conv_I8, () => Conv(typeof (long))},
	            {OpCodes.Conv_U, () => Conv(typeof (uint))},
	            {OpCodes.Conv_U1, () => Conv(typeof (byte))},
	            {OpCodes.Conv_U2, () => Conv(typeof (ushort))},
	            {OpCodes.Conv_U4, () => Conv(typeof (uint))},
	            {OpCodes.Conv_U8, () => Conv(typeof (ulong))},
	            {OpCodes.Conv_Ovf_I, () => ConvChecked(typeof (int))},
	            {OpCodes.Conv_Ovf_I1, () => ConvChecked(typeof (sbyte))},
	            {OpCodes.Conv_Ovf_I2, () => ConvChecked(typeof (short))},
	            {OpCodes.Conv_Ovf_I4, () => ConvChecked(typeof (int))},
	            {OpCodes.Conv_Ovf_I8, () => ConvChecked(typeof (long))},
	            {OpCodes.Conv_Ovf_U, () => ConvChecked(typeof (uint))},
	            {OpCodes.Conv_Ovf_U1, () => ConvChecked(typeof (byte))},
	            {OpCodes.Conv_Ovf_U2, () => ConvChecked(typeof (ushort))},
	            {OpCodes.Conv_Ovf_U4, () => ConvChecked(typeof (uint))},
	            {OpCodes.Conv_Ovf_U8, () => ConvChecked(typeof (ulong))},
	            {OpCodes.Conv_Ovf_I_Un, () => ConvChecked(typeof (int))},
	            {OpCodes.Conv_Ovf_I1_Un, () => ConvChecked(typeof (sbyte))},
	            {OpCodes.Conv_Ovf_I2_Un, () => ConvChecked(typeof (short))},
	            {OpCodes.Conv_Ovf_I4_Un, () => ConvChecked(typeof (int))},
	            {OpCodes.Conv_Ovf_I8_Un, () => ConvChecked(typeof (long))},
	            {OpCodes.Conv_Ovf_U_Un, () => ConvChecked(typeof (uint))},
	            {OpCodes.Conv_Ovf_U1_Un, () => ConvChecked(typeof (byte))},
	            {OpCodes.Conv_Ovf_U2_Un, () => ConvChecked(typeof (ushort))},
	            {OpCodes.Conv_Ovf_U4_Un, () => ConvChecked(typeof (uint))},
	            {OpCodes.Conv_Ovf_U8_Un, () => ConvChecked(typeof (ulong))},
	            {OpCodes.Conv_R4, () => ConvChecked(typeof (float))},
	            {OpCodes.Conv_R_Un, () => ConvChecked(typeof (float))},
	            {OpCodes.Conv_R8, () => ConvChecked(typeof (double))},
	            {OpCodes.Castclass, () => Conv((Type) instruction.Operand)},
	            {OpCodes.Newobj, NewObj},
	            {OpCodes.Newarr, NewArr},
	            {OpCodes.Box, Box},
	            {OpCodes.Call, Call},
	            {OpCodes.Callvirt, Call},
	            {OpCodes.Ceq, Ceq},
	            {OpCodes.Cgt, Cgt},
	            {OpCodes.Cgt_Un, Cgt},
	            {OpCodes.Clt, Clt},
	            {OpCodes.Clt_Un, Clt},
	            {OpCodes.Ret, Ret}
	        };
        }

        private void Ret()
        {
            instruction = null;
        }


        public Expression Process(Instruction start, Instruction last = null)
        {
            instruction = start;
            while (instruction != null && instruction != last)
            {
                Debug.WriteLine(instruction);
                var wasInstruction = instruction;
                table[instruction.OpCode]();
                if (wasInstruction == instruction)
                {
                    // branches or macros already advanced the instruction.
                    instruction = instruction.Next;
                }
            }
            var varUsageCounter = new CountLocalsVisitor(locals);
            foreach (var expr in stack)
                varUsageCounter.Visit(expr);

            var result = stack.Count == 0
                ? Expression.Empty()
                : stack.Count == 1
                    ? Pop()
                    : Expression.Block(varUsageCounter.UsedVars(), stack.Reverse());

            return result;
        }

        private void Clt()
        {
            var val1 = Pop();
            var val2 = Pop();

            Push(Expression.LessThan(val2, AdjustType(val1, val2.Type)));
        }

        private void Cgt()
        {
            var val1 = Pop();
            var val2 = Pop();

            Push(Expression.GreaterThan(val2, AdjustType(val1, val2.Type)));
        }

        private void Ceq()
        {
            var val1 = Pop();
            var val2 = Pop();

            Push(AdjustedBinaryExpression(val2, AdjustType(val1, val2.Type), ExpressionType.Equal));
        }

        private void Box()
        {
            var expression = Pop();
            var type = (Type)instruction.Operand;
            var constantExpression = expression as ConstantExpression;
            if (constantExpression != null && type.IsEnum)
                Push(Expression.Convert(Expression.Constant(Enum.ToObject(type, constantExpression.Value)), typeof(Enum)));
            else
                Push(Expression.Convert(expression, expression.Type.IsEnum ? typeof(Enum) : typeof(object)));
        }

        private void NewArr()
        {
            var operand = (Type)instruction.Operand;
            var expression = Pop();
            var size = expression as ConstantExpression;
            if (size != null && (int)size.Value == 0)
                Push(Expression.NewArrayInit(operand));
            else
                Push(Expression.NewArrayBounds(operand, expression));
        }

        private void NewObj()
        {
            var operand = (ConstructorInfo)instruction.Operand;
            Push(Expression.New(operand, GetArguments(operand)));
        }

        private void Or()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Or(val2, val1));
        }

        private void And()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.And(val2, val1));
        }

        private void ConvChecked(Type type)
        {
            Push(Expression.ConvertChecked(Pop(), type));
        }


        private void Conv(Type type)
        {
            var val1 = Pop();
            Push(Expression.Convert(val1, type));
        }

        private void Not()
        {
            var val = Pop();
            Push(Expression.Not(val));
        }

        private void Neg()
        {
            var val = Pop();
            Push(Expression.Negate(val));
        }

        private void Shr()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.RightShift(val2, val1));
        }

        private void Shl()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.LeftShift(val2, val1));
        }

        private void Xor()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.ExclusiveOr(val2, val1));
        }

        private void Rem()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Modulo(val2, val1));
        }

        private void Div()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Divide(val2, val1));
        }

        private void Mul_Ovf()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.MultiplyChecked(val2, val1));
        }

        private void Mul()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Multiply(val2, val1));
        }

        private void Sub_Ovf()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.SubtractChecked(val2, val1));
        }

        private void Sub()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Subtract(val2, val1));
        }

        private void Add_Ovf()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.AddChecked(val2, val1));
        }

        private void Add()
        {
            var val1 = Pop();
            var val2 = Pop();
            Push(Expression.Add(val2, val1));
        }

        private void Pop_()
        {
            Pop();
        }

        private void Dup()
        {
            Push(stack.Peek());
        }

        private void BneUn()
        {
            var val1 = Pop();
            ConditionalBranch(val => AdjustedBinaryExpression(val, val1, ExpressionType.NotEqual));
        }

        private void Beq()
        {
            var val1 = Pop();
            ConditionalBranch(val => AdjustedBinaryExpression(val, val1, ExpressionType.Equal));
        }

        private void Ble()
        {
            var val1 = Pop();
            ConditionalBranch(val => Expression.LessThanOrEqual(val, val1));
        }

        private void Blt()
        {
            var val1 = Pop();
            ConditionalBranch(val => Expression.LessThan(val, val1));
        }

        private void Bge()
        {
            var val1 = Pop();
            ConditionalBranch(val => Expression.GreaterThanOrEqual(val, val1));
        }

        private void Bgt()
        {
            var val1 = Pop();
            ConditionalBranch(val => Expression.GreaterThan(val, val1));
        }

        private void Brtrue()
        {
            var target = ((Instruction)instruction.Operand);

            if (target.OpCode == OpCodes.Ldsfld && IsCachedAnonymousMethodDelegate(target.Operand as FieldInfo))
            {
                Push(DelegateExtensions.ConvertMethod((MethodInfo)instruction.Next.Next.Operand));
                instruction = (Instruction)instruction.Operand;
            }
            else
            {
                ConditionalBranch(val => Expression.NotEqual(val, Default(val.Type)));
            }
        }

        private void Brfalse()
        {
            ConditionalBranch(val => Expression.Equal(val, Default(val.Type)));
        }

        private void Br()
        {
            instruction = (Instruction)instruction.Operand;
        }

        private void Ldstr()
        {
            Push(Expression.Constant(instruction.Operand));
        }

        private void LdFld()
        {
            var instance = Pop();
            Push(Expression.Field(instance, (FieldInfo)instruction.Operand));
        }

        private void Ldsfld()
        {
            var targetField = instruction.Operand as FieldInfo;

            if (IsCachedAnonymousMethodDelegate(targetField))
            {
                // do nothing.
            }
            else
            {
                Push(Expression.Field(null, (FieldInfo)instruction.Operand));
            }

        }

        private void LdNull()
        {
            Push(Expression.Constant(null));
        }

        private void LdElem()
        {
            var index = Pop();
            var array = Pop();
            Push(Expression.ArrayIndex(array, index));

        }

        private void Ldlen()
        {
            var array = Pop();
            Push(Expression.ArrayLength(array));

        }

        private void Ldarga()
        {
            var operand = (ParameterInfo)instruction.Operand;
            Push(args.Single(x => x.Name == operand.Name));
        }

        private void Nop()
        {
        }

        private void Break()
        {
        }

        private void LdLoca()
        {
            var operand = (LocalVariableInfo)instruction.Operand;
            if (!HandleInitStruct(operand))
                LdLoc(operand.LocalIndex);
        }


        private bool HandleInitStruct(LocalVariableInfo operand)
        {
            // when we have initialization of value type, handle it as type.Default()
            // for  example, (int?)null
            // ldloca.s   CS$0$0000
            // initobj    valuetype [mscorlib]System.Nullable`1<int32>
            // ldloc.s    CS$0$0000
            var initInstr = instruction.Next;
            if (initInstr == null || initInstr.OpCode != OpCodes.Initobj)
                return false;

            var ldlocInstr = initInstr.Next;
            if (ldlocInstr == null)
                return false;

            OpCode ldloc;
            switch (operand.LocalIndex)
            {
                case 0:
                    ldloc = OpCodes.Ldloc_0;
                    break;
                case 1:
                    ldloc = OpCodes.Ldloc_1;
                    break;
                case 2:
                    ldloc = OpCodes.Ldloc_2;
                    break;
                case 3:
                    ldloc = OpCodes.Ldloc_3;
                    break;
                default:
                    ldloc = instruction.OpCode == OpCodes.Ldloca ? OpCodes.Ldloc : OpCodes.Ldloc_S;
                    break;
            }

            if (ldlocInstr.OpCode != ldloc)
                return false;

            Type type = operand.LocalType;
            Push(Expression.Default(type));
            instruction = ldlocInstr.Next;
            return true;
        }

        private static bool IsCachedAnonymousMethodDelegate(FieldInfo field)
        {
            return field != null &&
                field.Name.Contains(CachedAnonymousMethodDelegate) &&
                Attribute.IsDefined(field, typeof(CompilerGeneratedAttribute), false);
        }

        private static BinaryExpression AdjustedBinaryExpression(Expression left, Expression right, ExpressionType expressionType)
        {
            left = ConvertEnumExpressionToUnderlyingType(left);
            right = ConvertEnumExpressionToUnderlyingType(right);

            return Expression.MakeBinary(expressionType, left, right);
        }

        private static Expression ConvertEnumExpressionToUnderlyingType(Expression expression)
        {
            if (expression.Type.IsEnum)
                return Expression.Convert(expression, expression.Type.GetEnumUnderlyingType());

            return expression;
        }

        static Expression Default(Type type)
        {
            if (type == typeof(bool))
                return Expression.Constant(false);
            if (type.IsValueType)
                return Expression.Default(type);
            return Expression.Constant(null, type);
        }

        void ConditionalBranch(Func<Expression, BinaryExpression> condition)
        {
            var val1 = Pop();
            var test = condition(val1);

            var ifTrue = (Instruction)instruction.Operand;
            var ifFalse = instruction.Next;

            Instruction common = GetJoinPoint(ifTrue, ifFalse);

            var ifFalseExpr = Clone().Process(ifFalse, common);
            var ifTrueExpr = Clone().Process(ifTrue, common);

            ifTrueExpr = AdjustType(ifTrueExpr, ifFalseExpr.Type);

            var expression = BuildConditionalBranch(test, val1, ifTrueExpr, ifFalseExpr);
            Push(expression);
            instruction = common;
        }

        private static Expression BuildConditionalBranch(BinaryExpression test, Expression val1, Expression ifTrue, Expression ifFalse)
        {
            var ifTrueConst = ifTrue as ConstantExpression;
            if (ifTrueConst != null && ifTrueConst.Value is bool)
            {
                if ((bool)ifTrueConst.Value)
                {
                    if (test.NodeType == ExpressionType.Equal ||
                        test.NodeType == ExpressionType.NotEqual ||
                        test.NodeType == ExpressionType.GreaterThan ||
                        test.NodeType == ExpressionType.GreaterThanOrEqual ||
                        test.NodeType == ExpressionType.LessThan ||
                        test.NodeType == ExpressionType.LessThanOrEqual)
                    {
                        return Expression.OrElse(test, ifFalse);
                    }
                }
                else
                {
                    switch (test.NodeType)
                    {
                        case ExpressionType.Equal:
                            return Expression.AndAlso(Expression.NotEqual(test.Left, test.Right), ifFalse);
                        case ExpressionType.NotEqual:
                            return Expression.AndAlso(Expression.Equal(test.Left, test.Right), ifFalse);
                        case ExpressionType.GreaterThan:
                            return Expression.AndAlso(Expression.LessThanOrEqual(test.Left, test.Right), ifFalse);
                        case ExpressionType.GreaterThanOrEqual:
                            return Expression.AndAlso(Expression.LessThan(test.Left, test.Right), ifFalse);
                        case ExpressionType.LessThan:
                            return Expression.AndAlso(Expression.GreaterThanOrEqual(test.Left, test.Right), ifFalse);
                        case ExpressionType.LessThanOrEqual:
                            return Expression.AndAlso(Expression.GreaterThan(test.Left, test.Right), ifFalse);
                    }
                }
            }
            if (test.NodeType == ExpressionType.NotEqual)
            {
                if (val1 == ifTrue && (test.Right is DefaultExpression || (test.Right is ConstantExpression && ((ConstantExpression)test.Right).Value == null)))
                {
                    return Expression.Coalesce(val1, ifFalse);
                }
            }
            return Expression.Condition(test, ifTrue, ifFalse, ifTrue.Type);
        }

        static Instruction GetJoinPoint(Instruction left, Instruction right)
        {
            return GetCommon(GetFlow(left), GetFlow(right));
        }

        static Instruction GetCommon(IEnumerable<Instruction> leftFlow, Stack<Instruction> rightFlow)
        {
            Instruction instruction = null;
            foreach (var left in leftFlow)
            {
                if (rightFlow.Count <= 0 || left != rightFlow.Pop())
                    break;

                instruction = left;
            }
            return instruction;
        }

        static Stack<Instruction> GetFlow(Instruction instruction)
        {
            var instructions = new Stack<Instruction>();
            while (instruction != null)
            {
                instructions.Push(instruction);

                if (instruction.OpCode.FlowControl == FlowControl.Return)
                    break;

                if (instruction.OpCode.FlowControl == FlowControl.Branch)
                {
                    instruction = (Instruction)instruction.Operand;
                }
                else if (instruction.OpCode.FlowControl == FlowControl.Cond_Branch)
                {
                    instruction = GetJoinPoint((Instruction)instruction.Operand, instruction.Next);
                }
                else
                {
                    instruction = instruction.Next;
                }
            }
            return instructions;
        }

        static Expression AdjustType(Expression expression, Type type)
        {
            var constantExpression = expression as ConstantExpression;
            if (constantExpression != null)
            {
                if (constantExpression.Value == null)
                {
                    return Expression.Constant(null, type);
                }
                if (expression.Type == typeof(int) && type == typeof(bool))
                {
                    return Expression.Constant(!Equals(constantExpression.Value, 0));
                }
            }
            else if (expression != null)
            {
                if (expression.Type == typeof(int) && type == typeof(bool))
                {
                    return Expression.NotEqual(expression, Expression.Constant(0));
                }

                if (!type.IsAssignableFrom(expression.Type) && expression.Type.IsEnum &&
                    expression.Type.GetEnumUnderlyingType() == type)
                {
                    return Expression.Convert(expression, type);
                }
            }
            return expression;
        }

        void StElem()
        {
            var value = Pop();
            var index = Pop();
            var array = Pop();

            var newArray = array as NewArrayExpression;
            if (newArray != null)
            {
                var expressions = CreateArrayInitExpressions(newArray, value);
                //UpdateLocals(newArray, Expression.NewArrayInit(array.Type.GetElementType(), expressions));
                //        void UpdateLocals(ParameterExpression oldExpression, ParameterExpression newExpression)
                //		{
                //			for (var i = 0; i < locals.Length; i++)
                //			{
                //				var local = locals[i];
                //				if (local == oldExpression)
                //				{
                //					locals[i] = newExpression;
                //				}
                //			}
                //		}
                //
                return;
            }

            throw new NotImplementedException();
        }

        static IEnumerable<Expression> CreateArrayInitExpressions(NewArrayExpression newArray, Expression value)
        {
            if (newArray.NodeType == ExpressionType.NewArrayInit)
            {
                var expressions = newArray.Expressions.ToList();
                expressions.Add(value);
                return expressions;
            }
            return new[] { value };
        }

        void LdC(int i)
        {
            Push(Expression.Constant(i));
        }

        void LdC(long i)
        {
            Push(Expression.Constant(i));
        }

        void LdC(float i)
        {
            Push(Expression.Constant(i));
        }

        void LdC(double i)
        {
            Push(Expression.Constant(i));
        }

        void Call()
        {
            var m = (MethodInfo)instruction.Operand;
            var mArgs = GetArguments(m);

            var instance = m.IsStatic ? null : GetCallInstance();
            Push(BuildMethodCallExpression(m, instance, mArgs));
        }

        private Expression GetCallInstance()
        {
            // we could have parameter assigned to local var inside call. for example, Ext.CreateTabPanel("title") -> 
            // IL_0040: ldfld Entropy.Ext.IExtFactory Ext
            // IL_0045: ldstr "title"
            // IL_004a: stloc.3   <- this assignment is inside call constructor
            // ... load other params
            // IL_009e: ldloc.3
            // IL_009f: callvirt Entropy.Ext.ITabPanel 

            var relocateAssignments = new Stack<Expression>();

            Expression result = Pop();
            while (result.NodeType == ExpressionType.Assign)
            {
                relocateAssignments.Push(result);
                result = Pop();
            }

            while (relocateAssignments.Any())
                stack.Push(relocateAssignments.Pop());
            return result;
        }

        Expression[] GetArguments(MethodBase m)
        {
            var parameterInfos = m.GetParameters();
            var mArgs = new Expression[parameterInfos.Length];
            for (var i = parameterInfos.Length - 1; i >= 0; i--)
            {
                mArgs[i] = Pop();
            }
            return mArgs;
        }

        Expression BuildMethodCallExpression(MethodInfo m, Expression instance, Expression[] arguments)
        {
            if (m.Name == "Add" && instance != null && typeof(IEnumerable).IsAssignableFrom(instance.Type))
            {
                var newExpression = instance as NewExpression;
                if (newExpression != null)
                {
                    var init = Expression.ListInit(newExpression, Expression.ElementInit(m, arguments));
                    //UpdateLocals(newExpression, init);
                    return init;
                }
                var initExpression = instance as ListInitExpression;
                if (initExpression != null)
                {
                    var initializers = initExpression.Initializers.ToList();
                    initializers.Add(Expression.ElementInit(m, arguments));
                    var init = Expression.ListInit(initExpression.NewExpression, initializers);
                    //UpdateLocals(initExpression, init);
                    return init;
                }
            }
            if (m.IsSpecialName && m.IsHideBySig)
            {
                if (m.Name.StartsWith("get_"))
                {
                    return Expression.Property(instance, m);
                }
                if (m.Name.StartsWith("op_"))
                {
                    ExpressionType type;
                    if (Enum.TryParse(m.Name.Substring(3), out type))
                    {
                        switch (arguments.Length)
                        {
                            case 1:
                                return Expression.MakeUnary(type, arguments[0], arguments[0].Type);
                            case 2:
                                return Expression.MakeBinary(type, arguments[0], arguments[1]);
                        }
                    }
                }
            }
            if (m.Name == "Concat" && m.DeclaringType == typeof(string))
            {
                var expressions = GetExpressionsForStringConcat(arguments);
                if (expressions.Count > 1)
                {
                    var expression = expressions[0];
                    for (var i = 1; i < expressions.Count; i++)
                    {
                        expression = Expression.Add(expression, expressions[i], StringConcat);
                    }
                    return expression;
                }
            }

            var parameters = m.GetParameters();
            for (int i = 0; i < parameters.Length; i++)
            {
                var parameter = parameters[i];
                var argument = arguments[i];
                var parameterType = parameter.ParameterType;
                arguments[i] = AdjustType(argument, parameterType);
            }

            if (instance != null)
                return Expression.Call(AdjustType(instance, m.DeclaringType), m, arguments);

            return Expression.Call(null, m, arguments);
        }

        static IList<Expression> GetExpressionsForStringConcat(Expression[] arguments)
        {
            if (arguments.Length == 1)
            {
                var array = arguments[0] as NewArrayExpression;
                if (array != null)
                {
                    if (array.NodeType == ExpressionType.NewArrayInit)
                    {
                        return array.Expressions;
                    }
                }
            }
            return arguments;
        }

        void LdLoc(int index)
        {
            Push(locals[index]);
        }

        void StLoc(int index)
        {
            var param = locals[index];
            var arg = Pop();
            arg = AdjustType(arg, param.Type);
            Push(Expression.Assign(param, arg));
        }

        void LdArg(int index)
        {
            Push(args[index]);
        }
    }

    internal class CountLocalsVisitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, int> locals;

        internal IEnumerable<ParameterExpression> UsedVars()
        {
            return locals.Where(pair => pair.Value > 0).Select(pair => pair.Key);
        }

        internal CountLocalsVisitor(ParameterExpression[] locals)
        {
            this.locals = locals.ToDictionary(e => e, e => 0);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            int count;
            if (locals.TryGetValue(node, out count))
            {
                locals[node] = count + 1;
            }
            return node;
        }
    }
}
